feat: cloud native file browser, settings Claude auth, deploy hardening
- Add native Cloud file browser with FileBrowser API integration
- Add cloud store, filebrowser-client, useAudioPlayer, useFileType composables
- Add Cloud components: FileGrid, FileCard, FileCardGrid, CloudToolbar
- Add Claude authentication section to Settings with OAuth status check
- Harden deploy script to preserve /aiui/ and claude-login.html
- Add nginx proxies for btcpay, homeassistant, filebrowser (HTTPS block)
- Add app configs for filebrowser, searxng, penpot in package.rs
- Update goal progress tracking with app aliases
- Improve mobile back button composable with ResizeObserver
- Update various views with cloud integration and UI refinements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:05:01 +00:00
|
|
|
export interface FileBrowserItem {
|
|
|
|
|
name: string
|
|
|
|
|
path: string
|
|
|
|
|
size: number
|
|
|
|
|
modified: string
|
|
|
|
|
isDir: boolean
|
|
|
|
|
type: string
|
|
|
|
|
extension: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface FileBrowserListResponse {
|
|
|
|
|
items: FileBrowserItem[]
|
|
|
|
|
numDirs: number
|
|
|
|
|
numFiles: number
|
|
|
|
|
sorting: { by: string; asc: boolean }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class FileBrowserClient {
|
|
|
|
|
private token: string | null = null
|
|
|
|
|
private baseUrl: string
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
this.baseUrl = `${window.location.origin}/app/filebrowser`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get isAuthenticated(): boolean {
|
|
|
|
|
return this.token !== null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async login(username = 'admin', password = 'admin'): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`${this.baseUrl}/api/login`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ username, password }),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) return false
|
|
|
|
|
const text = await res.text()
|
|
|
|
|
// FileBrowser returns the JWT as a plain string (possibly quoted)
|
|
|
|
|
this.token = text.replace(/^"|"$/g, '')
|
|
|
|
|
return true
|
|
|
|
|
} catch {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private headers(): Record<string, string> {
|
|
|
|
|
const h: Record<string, string> = {}
|
|
|
|
|
if (this.token) h['X-Auth'] = this.token
|
|
|
|
|
return h
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listDirectory(path: string): Promise<FileBrowserItem[]> {
|
|
|
|
|
const safePath = path.startsWith('/') ? path : `/${path}`
|
|
|
|
|
const res = await fetch(`${this.baseUrl}/api/resources${safePath}`, {
|
|
|
|
|
headers: this.headers(),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) throw new Error(`Failed to list directory: ${res.status}`)
|
|
|
|
|
const data: FileBrowserListResponse = await res.json()
|
|
|
|
|
return (data.items || []).map((item) => ({
|
|
|
|
|
...item,
|
|
|
|
|
extension: item.name.includes('.') ? item.name.split('.').pop()!.toLowerCase() : '',
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
downloadUrl(path: string): string {
|
|
|
|
|
const safePath = path.startsWith('/') ? path : `/${path}`
|
|
|
|
|
// Token is passed as query param for direct downloads (img src, audio src, etc.)
|
|
|
|
|
return `${this.baseUrl}/api/raw${safePath}?auth=${this.token}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async upload(dirPath: string, file: File): Promise<void> {
|
|
|
|
|
const safePath = dirPath.endsWith('/') ? dirPath : `${dirPath}/`
|
|
|
|
|
const encodedName = encodeURIComponent(file.name)
|
|
|
|
|
const res = await fetch(
|
|
|
|
|
`${this.baseUrl}/api/resources${safePath}${encodedName}?override=true`,
|
|
|
|
|
{
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: this.headers(),
|
|
|
|
|
body: file,
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const text = await res.text().catch(() => '')
|
|
|
|
|
throw new Error(`Upload failed (${res.status}): ${text}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createFolder(parentPath: string, name: string): Promise<void> {
|
|
|
|
|
const safePath = parentPath.endsWith('/') ? parentPath : `${parentPath}/`
|
|
|
|
|
const res = await fetch(`${this.baseUrl}/api/resources${safePath}${name}/`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: this.headers(),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) throw new Error(`Create folder failed: ${res.status}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteItem(path: string): Promise<void> {
|
|
|
|
|
const safePath = path.startsWith('/') ? path : `/${path}`
|
|
|
|
|
const res = await fetch(`${this.baseUrl}/api/resources${safePath}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: this.headers(),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) throw new Error(`Delete failed: ${res.status}`)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 07:05:14 +00:00
|
|
|
async getUsage(): Promise<{ totalSize: number; folderCount: number; fileCount: number }> {
|
|
|
|
|
if (!this.isAuthenticated) {
|
|
|
|
|
const ok = await this.login()
|
|
|
|
|
if (!ok) return { totalSize: 0, folderCount: 0, fileCount: 0 }
|
|
|
|
|
}
|
|
|
|
|
const res = await fetch(`${this.baseUrl}/api/resources/`, {
|
|
|
|
|
headers: this.headers(),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) return { totalSize: 0, folderCount: 0, fileCount: 0 }
|
|
|
|
|
const data: FileBrowserListResponse = await res.json()
|
|
|
|
|
const items = data.items || []
|
|
|
|
|
const folderCount = items.filter(i => i.isDir).length
|
|
|
|
|
const fileCount = items.filter(i => !i.isDir).length
|
|
|
|
|
const totalSize = items.reduce((sum, i) => sum + (i.size || 0), 0)
|
|
|
|
|
return { totalSize, folderCount, fileCount }
|
|
|
|
|
}
|
|
|
|
|
|
feat: cloud native file browser, settings Claude auth, deploy hardening
- Add native Cloud file browser with FileBrowser API integration
- Add cloud store, filebrowser-client, useAudioPlayer, useFileType composables
- Add Cloud components: FileGrid, FileCard, FileCardGrid, CloudToolbar
- Add Claude authentication section to Settings with OAuth status check
- Harden deploy script to preserve /aiui/ and claude-login.html
- Add nginx proxies for btcpay, homeassistant, filebrowser (HTTPS block)
- Add app configs for filebrowser, searxng, penpot in package.rs
- Update goal progress tracking with app aliases
- Improve mobile back button composable with ResizeObserver
- Update various views with cloud integration and UI refinements
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:05:01 +00:00
|
|
|
async rename(oldPath: string, newName: string): Promise<void> {
|
|
|
|
|
const safePath = oldPath.startsWith('/') ? oldPath : `/${oldPath}`
|
|
|
|
|
const dir = safePath.substring(0, safePath.lastIndexOf('/') + 1)
|
|
|
|
|
const res = await fetch(`${this.baseUrl}/api/resources${safePath}`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: {
|
|
|
|
|
...this.headers(),
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ destination: `${dir}${newName}` }),
|
|
|
|
|
})
|
|
|
|
|
if (!res.ok) throw new Error(`Rename failed: ${res.status}`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const fileBrowserClient = new FileBrowserClient()
|